<!DOCTYPE html><html class="appearance-auto" lang="zh-CN"><head><meta charset="UTF-8"><title>Windows TSF 学习笔记</title><meta name="description"><meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, initial-scale=1"><!-- Google Analytics --><!-- End Google Analytics -->
<!-- Baidu Analytics --><!-- End Baidu Analytics --><link rel="icon" href="/images/common/head.jpeg"><link rel="stylesheet" href="/style/common/bulma.css"><link rel="stylesheet" href="/style/base.css"><link rel="stylesheet" href="/style/common/helper.css"><script src="/js/common.js"></script><link rel="stylesheet" href="/style/post.css"><link rel="stylesheet" href="/style/themes/highlight-theme-light.css"><script src="/js/highlight.pack.js"></script><meta name="description" content="
引言
因为工作需要，需要实现基于TSF的输入法程序，但国内关于Windows TSF的资料太少，于是准备基于 libIME2 和 SampleIME 两个开源代码进行学习。本人非C++科班出身，如有错漏欢迎留言指正。

1. TSF (Text Service Framework) 简要介绍
Microsoft Windows 文本服务框架 （TSF） 是一种系统服务，可作为可再发行组件Windows。 TSF 提供简单且可缩放的框架，用于提供高级文本输入和自然语言技术。 可以在应用程序中启用 TSF，也可以作为 TSF 文本服务启用 TSF。 TSF 文本服务提供多语言支持，并提供键盘处理器、手写识别和语音识别等文本服务。
2. COM 组件介绍
COM component（COM组件）是微软公司为了计.."><meta name="generator" content="Hexo 6.0.0">
<style>.github-emoji { position: relative; display: inline-block; width: 1.2em; min-height: 1.2em; overflow: hidden; vertical-align: top; color: transparent; }  .github-emoji > span { position: relative; z-index: 10; }  .github-emoji img, .github-emoji .fancybox { margin: 0 !important; padding: 0 !important; border: none !important; outline: none !important; text-decoration: none !important; user-select: none !important; cursor: auto !important; }  .github-emoji img { height: 1.2em !important; width: 1.2em !important; position: absolute !important; left: 50% !important; top: 50% !important; transform: translate(-50%, -50%) !important; user-select: none !important; cursor: auto !important; } .github-emoji-fallback { color: inherit; } .github-emoji-fallback img { opacity: 0 !important; }</style>
<link rel="alternate" href="/atom.xml" title="录石匠" type="application/atom+xml">
</head><body class="is-flex is-flex-direction-column"><header class="header-widget is-flex-shrink-0 is-hidden-mobile"><div class="container is-fullhd is-flex is-justify-content-space-between is-align-items-center is-full-height"><section class="is-hidden-mobile is-flex-shrink-0"><h2><a href="/">录石匠's blog</a></h2></section><h3 class="is-hidden-mobile is-family-serif is-full-height is-flex is-align-items-center is-flex-shrink-0"><div class="is-full-height" id="postTopic"><p class="is-full-height is-flex-shrink-0 is-flex is-align-items-center is-justify-content-center">Windows TSF 学习笔记</p><p class="is-full-height is-flex-shrink-0 is-flex is-align-items-center is-justify-content-center">点击返回顶部</p></div></h3><aside class="is-flex-shrink-0"><h3 class="is-inline-block"><a href="/">首页</a></h3><h3 class="is-inline-block"><a href="/about">关于</a></h3><h3 class="is-inline-block"><a href="/archives">归档</a></h3></aside></div></header><header class="is-flex header-widget is-flex-shrink-0 is-align-items-center is-justify-content-center is-hidden-tablet"><h3 class="is-inline-block"><a href="/">首页</a></h3><h3 class="is-inline-block"><a href="/about">关于</a></h3><h3 class="is-inline-block"><a href="/archives">归档</a></h3></header><main><main class="container is-max-widescreen content section post-page pt-4 px-4"><div class="columns is-flex-desktop is-justify-content-center is-flex-direction-row-reverse"><div class="column is-3 is-hidden-mobile"><ol class="toc"><li class="toc-item toc-level-1"><a class="toc-link"><span class="toc-text">引言</span></a></li><li class="toc-item toc-level-1"><a class="toc-link"><span class="toc-text">1. TSF (Text Service Framework) 简要介绍</span></a></li><li class="toc-item toc-level-1"><a class="toc-link"><span class="toc-text">2. COM 组件介绍</span></a></li><li class="toc-item toc-level-1"><a class="toc-link"><span class="toc-text">3. DLL 介绍</span></a></li><li class="toc-item toc-level-1"><a class="toc-link"><span class="toc-text">4. TSF 输入法实现</span></a><ol class="toc-child"><li class="toc-item toc-level-2"><a class="toc-link" href="#4-1-TSF-%E7%A8%8B%E5%BA%8F%E6%B3%A8%E5%86%8C"><span class="toc-text">4.1 TSF 程序注册</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#4-1-1-DLL-%E6%B3%A8%E5%86%8C%E8%BF%87%E7%A8%8B"><span class="toc-text">4.1.1 DLL 注册过程</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#4-1-2-COM-%E7%BB%84%E4%BB%B6%E6%B3%A8%E5%86%8C"><span class="toc-text">4.1.2 COM 组件注册</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#4-1-3-%E8%BE%93%E5%85%A5%E6%B3%95-Profile-%E6%B3%A8%E5%86%8C"><span class="toc-text">4.1.3 输入法 Profile 注册</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#4-1-4-%E8%BE%93%E5%85%A5%E6%B3%95-Category"><span class="toc-text">4.1.4 输入法 Category</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#4-1-5-DllGetClassObject-%E6%8F%90%E4%BE%9B%E7%BB%99%E7%B3%BB%E7%BB%9F%E8%8E%B7%E5%8F%96%E8%BE%93%E5%85%A5%E6%B3%95%E7%A8%8B%E5%BA%8F%E5%AF%B9%E8%B1%A1%E7%9A%84%E5%87%BD%E6%95%B0"><span class="toc-text">4.1.5 DllGetClassObject 提供给系统获取输入法程序对象的函数</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#4-2-TSF-%E6%8E%A5%E5%8F%A3%E5%92%8C%E4%BA%8B%E4%BB%B6"><span class="toc-text">4.2 TSF 接口和事件</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#4-2-1-ITfTextInputProcessor-ITfTextInputProcessorEx"><span class="toc-text">4.2.1 ITfTextInputProcessor &amp; ITfTextInputProcessorEx</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#4-2-2-ITfThreadMgrEventSink"><span class="toc-text">4.2.2 ITfThreadMgrEventSink</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#4-2-3-ITfTextEditSink"><span class="toc-text">4.2.3 ITfTextEditSink</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#4-2-4-ITfKeyEventSink"><span class="toc-text">4.2.4 ITfKeyEventSink</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#4-2-5-ITfActiveLanguageProfileNotifySink"><span class="toc-text">4.2.5 ITfActiveLanguageProfileNotifySink</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#4-2-6-ITfThreadFocusSink"><span class="toc-text">4.2.6 ITfThreadFocusSink</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#4-2-6-%E5%8F%AF%E9%80%89-Sink%EF%BC%88SampleIME-%E4%B8%AD%E5%AE%9E%E7%8E%B0%EF%BC%8C%E4%BD%86%E4%B8%8D%E5%BF%85%E9%A1%BB%EF%BC%89"><span class="toc-text">4.2.6 可选 Sink（SampleIME 中实现，但不必须）</span></a><ol class="toc-child"><li class="toc-item toc-level-4"><a class="toc-link" href="#4-2-6-1-ITfFunctionProvider"><span class="toc-text">4.2.6.1 ITfFunctionProvider</span></a></li><li class="toc-item toc-level-4"><a class="toc-link" href="#4-2-6-2-ITfFnGetPreferredTouchKeyboardLayout"><span class="toc-text">4.2.6.2 ITfFnGetPreferredTouchKeyboardLayout</span></a></li></ol></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#4-3-TSF-%E4%B8%8A%E4%B8%8B%E6%96%87%E7%AE%A1%E7%90%86%E5%92%8C%E8%AE%BF%E9%97%AE"><span class="toc-text">4.3 TSF 上下文管理和访问</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#4-3-1-ITfContext"><span class="toc-text">4.3.1 ITfContext</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#4-3-2-ITfEditSession"><span class="toc-text">4.3.2 ITfEditSession</span></a></li></ol></li></ol></li></ol></div><div class="column is-9"><header class="my-4"><a href="/tags/C++"><i class="tag post-item-tag">C++</i></a><a href="/tags/TSF"><i class="tag post-item-tag">TSF</i></a><a href="/tags/Windows"><i class="tag post-item-tag">Windows</i></a></header><h1 class="mt-0 mb-1 is-family-serif" id="postTitle">Windows TSF 学习笔记</h1><time class="has-text-grey" datetime="2022-01-18T09:58:39.000Z">2022-01-18</time><article class="mt-2 post-content"><img src="/2022/01/18/tsf/head.jpg" class="">
<h1>引言</h1>
<p>因为工作需要，需要实现基于TSF的输入法程序，但国内关于Windows TSF的资料太少，于是准备基于 <a target="_blank" rel="noopener" href="https://github.com/EasyIME/libIME2">libIME2</a> 和 <a target="_blank" rel="noopener" href="https://github.com/Microsoft/Windows-classic-samples/tree/main/Samples/IME/cpp/SampleIME">SampleIME</a> 两个开源代码进行学习。本人非C++科班出身，如有错漏欢迎留言指正。</p>
<span id="more"></span>
<h1>1. TSF (Text Service Framework) 简要介绍</h1>
<p><a target="_blank" rel="noopener" href="https://docs.microsoft.com/zh-cn/windows/win32/tsf/text-services-framework">Microsoft Windows 文本服务框架 （TSF）</a> 是一种系统服务，可作为可再发行组件Windows。 TSF 提供简单且可缩放的框架，用于提供高级文本输入和自然语言技术。 可以在应用程序中启用 TSF，也可以作为 TSF 文本服务启用 TSF。 TSF 文本服务提供多语言支持，并提供键盘处理器、手写识别和语音识别等文本服务。</p>
<h1>2. COM 组件介绍</h1>
<p><a target="_blank" rel="noopener" href="https://docs.microsoft.com/zh-cn/windows/win32/com/registering-com-applications">COM component（COM组件）</a>是微软公司为了计算机工业的软件生产更加符合人类的行为方式开发的一种新的软件开发技术。在COM构架下，人们可以开发出各种各样的功能专一的组件，然后将它们按照需要组合起来，构成复杂的应用系统。由此带来的好处是多方面的：可以将系统中的组件用新的替换掉，以便随时进行系统的升级和定制；可以在多个应用系统中重复利用同一个组件；可以方便的将应用系统扩展到网络环境下；COM与语言，平台无关的特性使所有的程序员均可充分发挥自己的才智与专长编写组件模块。</p>
<h1>3. DLL 介绍</h1>
<p><a target="_blank" rel="noopener" href="https://docs.microsoft.com/zh-cn/office/client-developer/excel/developing-dlls">DLL（动态链接库）</a>一个DLL表示一组经过编译的代码，为可执行应用程序提供一些功能和数据。 库可以是静态链接的，也可以是动态链接的，两种情况下通常分别具有文件扩展名 .lib 和 .dll。 静态库（例如 C 运行时库）在编译时链接到应用程序，因此成为生成的可执行文件的一部分。 应用程序在需要 DLL 时（通常是在应用程序启动时）加载 DLL。 一个 DLL 可以加载并动态链接到另一个 DLL。</p>
<h1>4. TSF 输入法实现</h1>
<p>从微软提供的示例代码 SampleIME 看起，我认为 TSF 输入法程序的本质就是一个以 COM 组件形式编写实现了 TSF 接口的 DLL程序。在编写 COM 组件时，需要分配一个 CLSID，这里需要保证 CLSID 是在系统运行时唯一的即可（可以使用 Visual Studio 中的 GUID 创建工具生成），注册 COM 组件时指定该 CLSID，让系统可以根据 CLSID 直接获取 COM 组件的对象。在后续注册输入法 Profile 时也需要指定该 CLSID，让系统可以认为该 COM 组件是遵循 TSF 框架的输入法，当注册完毕后切换系统输入法时，就可以看到自己编写的输入法了。</p>
<h2 id="4-1-TSF-程序注册">4.1 TSF 程序注册</h2>
<h3 id="4-1-1-DLL-注册过程">4.1.1 DLL 注册过程</h3>
<p>示例代码 SampleIME ，程序入口应该是 DllMain.cpp ，但初次接触DLL程序编写的我还是有点懵，这啥也没做啊，就注册了自定义窗口，代码内容为：</p>
<pre><code>BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID pvReserved) {
    switch (dwReason) {
        case DLL_PROCESS_ATTACH:
            Global::dllInstanceHandle = hInstance;
            if (!InitializeCriticalSectionAndSpinCount(&amp;Global::CS, 0)) {
                return FALSE;
            }
            if (!Global::RegisterWindowClass()) {
                return FALSE;
            }
            break;
        case DLL_PROCESS_DETACH:
            DeleteCriticalSection(&amp;Global::CS);
            break;
        ...
    }
    return TRUE;
}
</code></pre>
<p>研究了一阵子才发现，DLL在被系统注册时执行函数不在这里，程序第一被注入时的入口在 Server.cpp</p>
<pre><code>STDAPI DllRegisterServer(void) { ... }
STDAPI DllUnregisterServer(void) { ... }
</code></pre>
<p>示例代码中还定义了 *.def 文件，该文件描述 DLL 所暴露出的函数，这里提供了注册时被系统调用的函数，所以在自行实现 TSF 输入法时应该注意需要创建类似的文件或者函数实现。</p>
<pre><code>LIBRARY SampleIME.dll
EXPORTS
        DllGetClassObject    PRIVATE // 提供 COM 调用时所获取的 Object
        DllCanUnloadNow      PRIVATE // 提示当前 DLL 是否可以被卸载
        DllRegisterServer    PRIVATE // 卸载 DLL
        DllUnregisterServer  PRIVATE // 注册 DLL
</code></pre>
<p>到这里，DLL 的注册过程全部完成了，TSF 输入法部分在程序实现后通过 DllGetClassObject 函数提供给系统，如果一切正常，那么 TSF 输入法就可以算完成了。</p>
<h3 id="4-1-2-COM-组件注册">4.1.2 COM 组件注册</h3>
<p>在 DLL 注册过程中，首先执行的操作就是 COM 组件的注册。这里引用 libIME2 的代码注释介绍一下 COM 组件的注册方法</p>
<blockquote>
<p>// write info of our COM text service component to the registry path:<br>
// HKEY_CLASS_ROOT\CLSID\{xxxx-xxxx-xxxx-xx…}<br>
// This reguires Administrator permimssion to write to the registery regsvr32<br>
// should be run with Administrator For 64 bit dll,<br>
// it seems that we need to write the key to a different path to make it coexist with 32 bit version:<br>
// HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Classes\CLSID{xxx-xxx-…}<br>
// Reference: <a target="_blank" rel="noopener" href="http://stackoverflow.com/questions/1105031/can-my-32-bit-and-64-bit-com-components-co-reside-on-the-same-machine">http://stackoverflow.com/questions/1105031/can-my-32-bit-and-64-bit-com-components-co-reside-on-the-same-machine</a></p>
</blockquote>
<pre><code>HKEY hkey = NULL;
if(::RegCreateKeyExW(HKEY_CLASSES_ROOT, "CLSID//xxxx-xx...", 0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &amp;hkey, NULL) == ERROR_SUCCESS) {
    // write name of our IME
    ::RegSetValueExW(hkey, NULL, 0, REG_SZ, (BYTE*)imeName, sizeof(wchar_t) * (wcslen(imeName) + 1));
    HKEY inProcServer32Key;
    if(::RegCreateKeyExW(hkey, L"InprocServer32", 0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &amp;inProcServer32Key, NULL) == ERROR_SUCCESS) {
        // store the path of our dll module in the registry
        ::RegSetValueExW(inProcServer32Key, NULL, 0, REG_SZ, (BYTE*)modulePath, (modulePathLen + 1) * sizeof(wchar_t));
        // write threading model
        wchar_t apartmentStr[] = L"Apartment";
        ::RegSetValueExW(inProcServer32Key, L"ThreadingModel", 0, REG_SZ, (BYTE*)apartmentStr, 10 * sizeof(wchar_t));
        ::RegCloseKey(inProcServer32Key);
    }
}
::RegCloseKey(hkey);
</code></pre>
<h3 id="4-1-3-输入法-Profile-注册">4.1.3 输入法 Profile 注册</h3>
<p>输入法 Profile 注册是为了让我们自己编写的输入法程序能够被系统正确的识别，第一步先获取系统中输入法 Profile 管理器对象，该对象也是 COM 组件，获取方法如下</p>
<pre><code>ITfInputProcessorProfileMgr *pITfInputProcessorProfileMgr = nullptr;
CoCreateInstance(CLSID_TF_InputProcessorProfiles, NULL, CLSCTX_INPROC_SERVER,
                 IID_ITfInputProcessorProfileMgr, (void**)&amp;pITfInputProcessorProfileMgr);
</code></pre>
<p>当我们得到系统的输入法 Profile 管理器对象后就可以注册程序为输入法程序了。这里需要注意的是，我们还需要给输入法程序的 Profile 分配一个 CLSID，这个 CLSID 不可以和之前给主程序分配的 CLSID 一样。（这里我没能读懂的是输入法 Icon 的 index 这个参数到底该如何配置，如果有知道的大佬欢迎留言告知）</p>
<pre><code>/* SampleIMECLSID 示例
CLSID SampleIMECLSID = { 
    0xd2291a80,
    0x84d8,
    0x4641,
    { 0x9a, 0xb2, 0xbd, 0xd1, 0x47, 0x2c, 0x84, 0x6b }
}; */
pITfInputProcessorProfileMgr-&gt;RegisterProfile(Global::SampleIMECLSID,
    TEXTSERVICE_LANGID,
    Global::SampleIMEGuidProfile,
    L"输入法名称...",
    lenOfImeName,
    L"DLL Path ...",
    lenOfPath,
    (UINT)TEXTSERVICE_ICON_INDEX, NULL, 0, TRUE, 0);
</code></pre>
<h3 id="4-1-4-输入法-Category">4.1.4 输入法 Category</h3>
<p>当 Profile 注册完成后，程序可以被系统认为是一个输入法程序了，但系统加载输入法的时候还需要一些信息来知道但其概念输入法程序可以在哪些时候被调用，这时就需要告诉输入法管理器我们的输入法可以支持哪些环境下运行。首先需要获取系统 ITfCategoryMgr COM 对象，然后使用主程序的 CLSID 作为 key 告知系统“这个输入法”支持什么。<br>
首先确认一下我们的输入法是已哪种方式进行输入的（这里建议单选一个注册，注册多个我没试过）：</p>
<table>
<thead>
<tr>
<th>GUID</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>GUID_TFCAT_TIP_KEYBOARD</td>
<td>键盘输入</td>
</tr>
<tr>
<td>GUID_TFCAT_TIP_SPEECH</td>
<td>语音输入</td>
</tr>
<tr>
<td>GUID_TFCAT_TIP_HANDWRITING</td>
<td>手写输入</td>
</tr>
</tbody>
</table>
<p>然后告知系统我们的输入法支持哪些标准（可多选）：</p>
<table>
<thead>
<tr>
<th>GUID</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>GUID_TFCAT_DISPLAYATTRIBUTEPROVIDER</td>
<td>如果Text Service需要支持显示属性信息（display attribute infomation）需要注册这个容器</td>
</tr>
<tr>
<td>GUID_TFCAT_TIPCAP_SECUREMODE</td>
<td>支持安全模式。</td>
</tr>
<tr>
<td>GUID_TFCAT_TIPCAP_UIELEMENTENABLED</td>
<td>支持UI元素。</td>
</tr>
<tr>
<td>GUID_TFCAT_TIPCAP_INPUTMODECOMPARTMENT</td>
<td>输入法是一个COM</td>
</tr>
<tr>
<td>GUID_TFCAT_TIPCAP_COMLESS</td>
<td>不需要COM也可以激活。</td>
</tr>
<tr>
<td>GUID_TFCAT_TIPCAP_WOW16</td>
<td>可以在16位的任务上激活。</td>
</tr>
<tr>
<td>GUID_TFCAT_TIPCAP_IMMERSIVESUPPORT</td>
<td>Win8以后开始支持：已经通过测试，可以正常运行在Windows应用商店程序中。</td>
</tr>
<tr>
<td>GUID_TFCAT_TIPCAP_SYSTRAYSUPPORT</td>
<td>win8以后开始支持：支持在系统托盘中。这个使于那些未设置 TF_IPP_CAPS_IMMERSIVESUPPORT 标志，但是仍然与系统托盘兼容。</td>
</tr>
</tbody>
</table>
<pre><code>// 获取系统中的 ITfCategoryMgr 对象
ITfCategoryMgr* pCategoryMgr = nullptr;
CoCreateInstance(CLSID_TF_CategoryMgr, NULL, CLSCTX_INPROC_SERVER, IID_ITfCategoryMgr, (void**)&amp;pCategoryMgr);

// SampleIME 将 GUID 放在一个数组里 然后循环调用 ITfCategoryMgr::RegisterCategory
pCategoryMgr-&gt;RegisterCategory(Global::SampleIMECLSID, guid, Global::SampleIMECLSID);
</code></pre>
<h3 id="4-1-5-DllGetClassObject-提供给系统获取输入法程序对象的函数">4.1.5 DllGetClassObject 提供给系统获取输入法程序对象的函数</h3>
<p>关于 COM 组件编写的更多内容可以参考 <a target="_blank" rel="noopener" href="https://blog.csdn.net/woshinia/article/details/22300089">C++ COM组件的编写</a></p>
<blockquote>
<p>COM组件的DLLs必须输出一个叫DllGetClassObject的函数。由这个函数为CmyInterface创建类工厂并返回一个对它的引用。然后我们调用CoCreateInstance为进程内COM创建类工厂，接着调用DllGetClassObject。这个类工厂有一个方法是CreateInstance，由这个方法创建对象并返回对它的引用。</p>
</blockquote>
<p>具体函数实现在 <a target="_blank" rel="noopener" href="https://github.com/Microsoft/Windows-classic-samples/tree/main/Samples/IME/cpp/SampleIME">SampleIME</a> 和 <a target="_blank" rel="noopener" href="https://github.com/EasyIME/libIME2">libIME2</a> 源码里都有，这不是输入法核心内容就不多赘述了。这个函数获取的就是提供给系统调用的输入法对象，当完成这步之后，输入法就可以真正意义上被系统调用了，接下来就准备开始研究输入法核心代码是如何实现的。</p>
<pre><code>HRESULT DllGetClassObject(
  [in]  REFCLSID rclsid,
  [in]  REFIID   riid,
  [out] LPVOID   *ppv
);
</code></pre>
<h2 id="4-2-TSF-接口和事件">4.2 TSF 接口和事件</h2>
<p>2022年1月19号 更新：发现一篇博文 <a target="_blank" rel="noopener" href="https://blog.csdn.net/m0_51158844/article/details/121450853">山东大学软件工程应用于实践——WeaselTSF（二）</a>（果然科班出身的比我啃代码理解要更为专业）<br>
2022年1月20号 更新：上面那个博文大量引用了 <a target="_blank" rel="noopener" href="https://yangyuan.github.io/post/2015-01-08-zh-meow-ime/">逗比的输入法实现</a> 里的内容，饶了一圈…</p>
<h3 id="4-2-1-ITfTextInputProcessor-ITfTextInputProcessorEx">4.2.1 ITfTextInputProcessor &amp; ITfTextInputProcessorEx</h3>
<p>参考 SampleIME 和 libIME2 两个项目的源码可以发现，两个主程序的 Class 都实现了 ITfTextInputProcessor 和 ITfTextInputProcessorEx 接口，这两个接口中需要实现 3 个函数：</p>
<pre><code>// ITfTextInputProcessor
STDMETHODIMP Activate(ITfThreadMgr *pThreadMgr, TfClientId tfClientId);
STDMETHODIMP Deactivate();

// ITfTextInputProcessorEx
STDMETHODIMP ActivateEx(ITfThreadMgr *pThreadMgr, TfClientId tfClientId, DWORD dwFlags);
</code></pre>
<p>有意思的是 SampleIME 中 Activate 的实现是直接调用 ActivateEx，但 libIME2 项目中直接舍弃了 ActivateEx 的 dwFlags 参数。至于这个参数的作用，暂且不谈。<br>
当切换到当前输入法时，会调用 Activate 或者 ActivateEx 函数，当输入法切换到其他输入法时会调用 Deactivate 函数。<br>
现在我们看一下 SampleIME 都在 Activate 里做了什么（因为这个项目时微软提供的，而libIME2仅仅是以最小代价创建TSF程序的库，所以看SampleIME可以搞懂我们如果实现一个输入法需要做些什么）</p>
<pre><code>STDAPI CSampleIME::ActivateEx(ITfThreadMgr *pThreadMgr, TfClientId tfClientId, DWORD dwFlags) {
    // 缓存对象
    _pThreadMgr = pThreadMgr;
    _pThreadMgr-&gt;AddRef();

    _tfClientId = tfClientId;
    _dwActivateFlags = dwFlags;

    // 初始化 ThreadMgrEventSink
    if (!_InitThreadMgrEventSink()) { goto ExitError; }

    // 初始化 TextEditSink
    ITfDocumentMgr* pDocMgrFocus = nullptr;
    if (SUCCEEDED(_pThreadMgr-&gt;GetFocus(&amp;pDocMgrFocus)) &amp;&amp; (pDocMgrFocus != nullptr)) {
        _InitTextEditSink(pDocMgrFocus);
        pDocMgrFocus-&gt;Release();
    }

    // 初始化 KeyEventSink
    if (!_InitKeyEventSink()) { goto ExitError; }

    // 初始化 ActiveLanguageProfileNotifySink
    if (!_InitActiveLanguageProfileNotifySink()) { goto ExitError; }

    // 初始化 ThreadFocusSink
    if (!_InitThreadFocusSink()) { goto ExitError; }

    // 初始化 DisplayAttributeGuidAtom
    if (!_InitDisplayAttributeGuidAtom()) { goto ExitError; }

    // 初始化 FunctionProviderSink
    if (!_InitFunctionProviderSink()) { goto ExitError; }

    // 初始化 TextProcessorEngine
    if (!_AddTextProcessorEngine()) { goto ExitError; }
    return S_OK;
ExitError:
    Deactivate();
    return E_FAIL;
}
</code></pre>
<p>在 SampleIME 中，当输入法被用户选中后做了很多初始化相关工作，接下来让我们来一个一个分析它们都是做什么的。</p>
<h3 id="4-2-2-ITfThreadMgrEventSink">4.2.2 ITfThreadMgrEventSink</h3>
<p>ITfThreadMgrEventSink 是一个管理在不同文本编辑器中输入法内容的接口，它提供了以下函数接口</p>
<pre><code>// 当系统第一次将输入法唤起关联编辑器 之前 会调用
STDMETHODIMP OnInitDocumentMgr(_In_ ITfDocumentMgr *pDocMgr);

// 当系统最后一次将输入法与编辑器解除关联 之后 会调用
STDMETHODIMP OnUninitDocumentMgr(_In_ ITfDocumentMgr *pDocMgr);

// 当输入法与编辑器“关联”时调用
STDMETHODIMP OnSetFocus(_In_ ITfDocumentMgr *pDocMgrFocus, _In_ ITfDocumentMgr *pDocMgrPrevFocus);

// 系统将输入法状态缓存时调用
STDMETHODIMP OnPushContext(_In_ ITfContext *pContext);

// 系统将输入法状态移除时调用
STDMETHODIMP OnPopContext(_In_ ITfContext *pContext);
</code></pre>
<p>ITfThreadMgrEventSink 从接口描述上看可以提供更丰富的输入法功能，但有点遗憾的是在 SampleIME 项目中仅实现了 OnSetFocus 函数，其他均没有实现。不过从学习的角度那也是足够了，让我们看看它在 OnSetFocus 中做了哪些事情</p>
<pre><code>// 利用 ITfDocumentMgr 初始化 TextEditSink
_InitTextEditSink(pDocMgrFocus);

// 变换输入法“状态栏”中的信息
_UpdateLanguageBarOnSetFocus(pDocMgrFocus);

// 处理 candidate list 窗口状态
// We have to hide/unhide candidate list depending on whether they are associated with pDocMgrFocus.
if (_pCandidateListUIPresenter) {
    ITfDocumentMgr* pCandidateListDocumentMgr = nullptr;
    ITfContext* pTfContext = _pCandidateListUIPresenter-&gt;_GetContextDocument();
    if ((nullptr != pTfContext) &amp;&amp; SUCCEEDED(pTfContext-&gt;GetDocumentMgr(&amp;pCandidateListDocumentMgr))) {
        if (pCandidateListDocumentMgr != pDocMgrFocus) {
            _pCandidateListUIPresenter-&gt;OnKillThreadFocus();
        } else  {
            _pCandidateListUIPresenter-&gt;OnSetThreadFocus();
        }
        pCandidateListDocumentMgr-&gt;Release();
    }
}

// 将上一个 ITfDocumentMgr 对象释放 COM
if (_pDocMgrLastFocused) {
    _pDocMgrLastFocused-&gt;Release();
    _pDocMgrLastFocused = nullptr;
}

// 将当前 ITfDocumentMgr 对象的引用加 1
_pDocMgrLastFocused = pDocMgrFocus;
if (_pDocMgrLastFocused) {
    _pDocMgrLastFocused-&gt;AddRef();
}
</code></pre>
<p>ITfThreadMgrEventSink 被我们写的程序实现后是需要向输入法框架注册使用的，否则不会生效， 注册方法：</p>
<pre><code>// 从 ITfTextInputProcessor::Activate 中获取的 ITfThreadMgr 对象中获取 ITfSource
ITfSource* pSource = nullptr;
_pThreadMgr-&gt;QueryInterface(IID_ITfSource, (void **)&amp;pSource);

// 使用 ITfSource 激活自定义 ITfThreadMgrEventSink
pSource-&gt;AdviseSink(IID_ITfThreadMgrEventSink, (ITfThreadMgrEventSink *)this, &amp;_threadMgrEventSinkCookie);
</code></pre>
<h3 id="4-2-3-ITfTextEditSink">4.2.3 ITfTextEditSink</h3>
<p>ITfTextEditSink 仅提供一个函数，且该函数仅在编辑操作完成时调用。</p>
<pre><code>STDMETHODIMP OnEndEdit(__RPC__in_opt ITfContext *pContext, TfEditCookie ecReadOnly, __RPC__in_opt ITfEditRecord *pEditRecord);
</code></pre>
<p>这里提供 SampleIME 实现代码，从代码上看，应该时如果编辑操作结束后如果选中内容发生变化需要判断是否结束当前编辑操作（不是很理解）</p>
<pre><code>STDAPI CSampleIME::OnEndEdit(__RPC__in_opt ITfContext* pContext, TfEditCookie ecReadOnly, __RPC__in_opt ITfEditRecord* pEditRecord) {
    // did the selection change?
    // The selection change includes the movement of caret as well. 
    // The caret position is represent as the empty selection range when
    // there is no selection.
    if (pEditRecord == nullptr) {
        return E_INVALIDARG;
    }
    BOOL isSelectionChanged;
    if (SUCCEEDED(pEditRecord-&gt;GetSelectionStatus(&amp;isSelectionChanged)) &amp;&amp;
        isSelectionChanged) {
        // If the selection is moved to out side of the current composition,
        // we terminate the composition. This TextService supports only one
        // composition in one context object.
        if (_IsComposing()) {
            TF_SELECTION tfSelection;
            ULONG fetched = 0;

            if (pContext == nullptr) { return E_INVALIDARG;  }
            if (FAILED(pContext-&gt;GetSelection(ecReadOnly, TF_DEFAULT_SELECTION, 1, &amp;tfSelection, &amp;fetched)) || fetched != 1) {
                return S_FALSE;
            }

            ITfRange* pRangeComposition = nullptr;
            if (SUCCEEDED(_pComposition-&gt;GetRange(&amp;pRangeComposition))) {
                if (!_IsRangeCovered(ecReadOnly, tfSelection.range, pRangeComposition)) {
                    _EndComposition(pContext);
                }
                pRangeComposition-&gt;Release();
            }
            tfSelection.range-&gt;Release();
        }
    }
    return S_OK;
}
</code></pre>
<p>再给大家提供 libIME2 中的实现，对应看着，如果有那位大佬知道为什么要这么做了欢迎留言告知（虽然评论系统还没搞好<span class="github-emoji"><span>😂</span><img src="https://github.githubassets.com/images/icons/emoji/unicode/1f602.png?v8" aria-hidden="true" onerror="this.parent.classList.add('github-emoji-fallback')"></span>）</p>
<pre><code>STDMETHODIMP TextService::OnEndEdit(ITfContext *pContext, TfEditCookie ecReadOnly, ITfEditRecord *pEditRecord) {
    // This method is called by the TSF whenever an edit operation ends.
    // It's possible for a document to have multiple composition strings at the
    // same time and it's possible for other text services to edit the same
    // document. Though such a complicated senario rarely exist, it indeed happen.

    // NOTE: I don't really know why this is needed and tests yielded no obvious effect
    // of this piece of code, but from MS TSF samples, this is needed.
    BOOL selChanged;
    if(pEditRecord-&gt;GetSelectionStatus(&amp;selChanged) == S_OK) {
        if(selChanged &amp;&amp; isComposing()) {
            // we need to check if current selection is in our composition string.
            // if after others' editing the selection (insertion point) has been changed and
            // fell outside our composition area, terminate the composition.
            TF_SELECTION selection;
            ULONG selectionNum;
            if(pContext-&gt;GetSelection(ecReadOnly, TF_DEFAULT_SELECTION, 1, &amp;selection, &amp;selectionNum) == S_OK) {
                ComPtr&lt;ITfRange&gt; compRange;
                if(composition_-&gt;GetRange(&amp;compRange) == S_OK) {
                    // check if two ranges overlaps
                    // check if current selection is covered by composition range
                    LONG compareResult1;
                    LONG compareResult2;
                    if(compRange-&gt;CompareStart(ecReadOnly, selection.range, TF_ANCHOR_START, &amp;compareResult1) == S_OK
                        &amp;&amp; compRange-&gt;CompareEnd(ecReadOnly, selection.range, TF_ANCHOR_END, &amp;compareResult2) == S_OK) {
                        if(compareResult1 == +1 || compareResult2 == -1) {
                            // the selection is not entirely in composion
                            // end compositon here
                            endComposition(pContext);
                        }
                    }
                }
                selection.range-&gt;Release();
            }
        }
    }
    return S_OK;
}
</code></pre>
<p>ITfTextEditSink 的注册方法：</p>
<pre><code>// 从 ITfDocumentMgr 对象中获取 ITfContext
ITfContext* _pTextEditSinkContext = nullptr;
pDocMgr-&gt;GetTop(&amp;_pTextEditSinkContext);

// 从 ITfContext 对象中获取 ITfSource
ITfSource* pSource = nullptr;
_pTextEditSinkContext-&gt;QueryInterface(IID_ITfSource, (void **)&amp;pSource);

// 通过 ITfSource 对象激活 ITfTextEditSink
pSource-&gt;AdviseSink(IID_ITfTextEditSink, (ITfTextEditSink *)this, &amp;_textEditSinkCookie);
</code></pre>
<h3 id="4-2-4-ITfKeyEventSink">4.2.4 ITfKeyEventSink</h3>
<p>ITfKeyEventSink 是一个用于管理用户输入操作的接口，它提供了以下函数</p>
<pre><code>// 输入法焦点发生变化时调用
STDMETHODIMP OnSetFocus(BOOL fForeground);

// 在用户按下后“可能”被调用，TSF 当时应该是像让当前函数作为按键预处理函数以提供更好的响应操作
// 但因为某些原因，部分编辑器不支持该函数，导致在编写代码时不建议建议在该函数处理复杂事件
// 如果被调用时， pIsEaten 作为输出参数，如果为 TRUE，系统将继续调用 OnKeyDown，否则不处理
STDMETHODIMP OnTestKeyDown(ITfContext *pContext, WPARAM wParam, LPARAM lParam, BOOL *pIsEaten);

// 当用户按下某一个按键时且 OnTestKeyDown 的 pIsEaten 为 TRUE 时调用
STDMETHODIMP OnKeyDown(ITfContext *pContext, WPARAM wParam, LPARAM lParam, BOOL *pIsEaten);

// 当用户释放某一个按键时被调用 同 OnTestKeyDown
STDMETHODIMP OnTestKeyUp(ITfContext *pContext, WPARAM wParam, LPARAM lParam, BOOL *pIsEaten);

// 当用户释放某一个按键时且 OnTestKeyDown 的 pIsEaten 为 TRUE 时调用
STDMETHODIMP OnKeyUp(ITfContext *pContext, WPARAM wParam, LPARAM lParam, BOOL *pIsEaten);

// 当输入法注册的热键被触发时系统调用该函数
STDMETHODIMP OnPreservedKey(ITfContext *pContext, REFGUID rguid, BOOL *pIsEaten);
</code></pre>
<p>关于 ITfKeyEventSink 中的函数实现，在当前段落不做展开探讨，这和输入法的目的、和输入法的处理逻辑相关，和框架的学习关联不是很大，暂且放一放，之后学习输入法处理逻辑时会详细的查看。 ITfKeyEventSink 的注册方法：</p>
<pre><code>// 依然时通过 ITfThreadMgr 对象处理，获得 ITfKeystrokeMgr 对象
ITfKeystrokeMgr* pKeystrokeMgr = nullptr;
_pThreadMgr-&gt;QueryInterface(IID_ITfKeystrokeMgr, (void **)&amp;pKeystrokeMgr);

// 通过 ITfKeystrokeMgr 对象注册 ITfKeyEventSink
pKeystrokeMgr-&gt;AdviseKeyEventSink(_tfClientId, (ITfKeyEventSink *)this, TRUE);
</code></pre>
<p>输入法的系统热键注册方法，注册系统热键后就可以被输入法获取了：</p>
<pre><code>// 通过 ITfThreadMgr 对象获取 ITfKeystrokeMgr 对象
ITfKeystrokeMgr *pKeystrokeMgr = nullptr;
pThreadMgr-&gt;QueryInterface(IID_ITfKeystrokeMgr, (void **)&amp;pKeystrokeMgr);

// 定义快捷键
TF_PRESERVEDKEY preservedKey;
preservedKey.uVKey = VK_SPACE;
preservedKey.uModifiers = TF_MOD_SHIFT;
preservedKey.uModifiers &amp;= 0xffff; // 防止数据溢出

// tfClientId ITfTextInputProcessor::Activate 中获取的
// preservedKey_Guid 也需要定义一个全局唯一的 GUID
pKeystrokeMgr-&gt;PreserveKey(tfClientId, preservedKey_Guid, &amp;preservedKey, preservedKey_Description, preservedKey_Description_Length);
    
</code></pre>
<h3 id="4-2-5-ITfActiveLanguageProfileNotifySink">4.2.5 ITfActiveLanguageProfileNotifySink</h3>
<p>ITfActiveLanguageProfileNotifySink 中提供的函数接口：</p>
<pre><code>// 当输入法 Language Profile 状态变化后被调用
STDMETHODIMP OnActivated(_In_ REFCLSID clsid, _In_ REFGUID guidProfile, _In_ BOOL isActivated);
</code></pre>
<p>SampleIME 项目中支持 3 种类型输入方式切换：英文输入、中文输入和全角半角符号切换。当输入法输入方式发生了变化就需要做对应的处理。 ITfActiveLanguageProfileNotifySink 的注册方法：</p>
<pre><code>// 通过 ITfThreadMgr 获取 ITfSource 对象
ITfSource* pSource = nullptr;
_pThreadMgr-&gt;QueryInterface(IID_ITfSource, (void **)&amp;pSource);

// 使用 ITfSource 对象注册 ITfActiveLanguageProfileNotifySink
pSource-&gt;AdviseSink(IID_ITfActiveLanguageProfileNotifySink, (ITfActiveLanguageProfileNotifySink *)this, &amp;_activeLanguageProfileNotifySinkCookie);
</code></pre>
<p>如何定义一个输入法类型在之后看 ITfLangBarItemButton 如何实现的时候会详细讲解，这里暂且不提</p>
<h3 id="4-2-6-ITfThreadFocusSink">4.2.6 ITfThreadFocusSink</h3>
<p>被输入线程焦点（活动窗口）管理，它提供了两个函数接口：</p>
<pre><code>STDMETHODIMP OnSetThreadFocus();
STDMETHODIMP OnKillThreadFocus();
</code></pre>
<p>这两个接口都是输入法在不同线程直接切换时被调用，看起来和 ITfThreadMgrEventSink::OnSetFocus 重复了，但从示例项目看这里是做了和窗口相关操作，而 ITfThreadMgrEventSink::OnSetFocus 做了数据清理相关操作，这就仁者见仁智者见智了吧。</p>
<h3 id="4-2-6-可选-Sink（SampleIME-中实现，但不必须）">4.2.6 可选 Sink（SampleIME 中实现，但不必须）</h3>
<p>因为非必须，暂且不聊</p>
<h4 id="4-2-6-1-ITfFunctionProvider">4.2.6.1 ITfFunctionProvider</h4>
<p>SampleIME 在 ITfFunctionProvider 的函数实现中提供了 ITfFnSearchCandidateProvider 对象的获取（好像是微软提供的候选词在搜索栏中的一些处理，不是很明白）</p>
<h4 id="4-2-6-2-ITfFnGetPreferredTouchKeyboardLayout">4.2.6.2 ITfFnGetPreferredTouchKeyboardLayout</h4>
<p>ITfFnGetPreferredTouchKeyboardLayout 提供了触摸键盘的布局获取（可能是Win8 当时在移动设备上的支持）</p>
<h2 id="4-3-TSF-上下文管理和访问">4.3 TSF 上下文管理和访问</h2>
<h3 id="4-3-1-ITfContext">4.3.1 ITfContext</h3>
<p>ITfContext 接口由TSF管理器实现，并由应用程序和文本服务用于访问编辑上下文。</p>
<blockquote>
<p>The ITfContext interface is implemented by the TSF manager and used by applications and text services to access an edit context. &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; — <a target="_blank" rel="noopener" href="https://docs.microsoft.com/en-us/windows/win32/api/msctf/nn-msctf-itfcontext">ITfContext interface (msctf.h)</a></p>
</blockquote>
<p>我们不能主动生成 ITfContext 对象，必须通过 ITfThreadMgr 对象获取或者通过 ITfThreadMgrEventSink 函数接口获得。ITfContext 提供了以下函数：</p>
<pre><code>HRESULT RequestEditSession(TfClientId tid, __RPC__in_opt ITfEditSession *pes, DWORD dwFlags, __RPC__out HRESULT *phrSession);
HRESULT InWriteSession(TfClientId tid, __RPC__out BOOL *pfWriteSession);
HRESULT GetSelection(TfEditCookie ec, ULONG ulIndex, ULONG ulCount, __RPC__out_ecount_part(ulCount, *pcFetched) TF_SELECTION *pSelection, __RPC__out ULONG *pcFetched);
HRESULT SetSelection(TfEditCookie ec, ULONG ulCount, __RPC__in_ecount_full(ulCount) const TF_SELECTION *pSelection);
HRESULT GetStart(TfEditCookie ec, __RPC__deref_out_opt ITfRange **ppStart);
HRESULT GetEnd(TfEditCookie ec, __RPC__deref_out_opt ITfRange **ppEnd);
HRESULT GetActiveView(__RPC__deref_out_opt ITfContextView **ppView);
HRESULT EnumViews(__RPC__deref_out_opt IEnumTfContextViews **ppEnum);
HRESULT GetStatus(__RPC__out TF_STATUS *pdcs);
HRESULT GetProperty(__RPC__in REFGUID guidProp, __RPC__deref_out_opt ITfProperty **ppProp);
HRESULT GetAppProperty(__RPC__in REFGUID guidProp, __RPC__deref_out_opt ITfReadOnlyProperty **ppProp);
HRESULT TrackProperties(__RPC__in_ecount_full(cProp) const GUID **prgProp, ULONG cProp, __RPC__in_ecount_full(cAppProp) const GUID **prgAppProp, ULONG cAppProp, __RPC__deref_out_opt ITfReadOnlyProperty **ppProperty);
HRESULT EnumProperties(__RPC__deref_out_opt IEnumTfProperties **ppEnum);
HRESULT GetDocumentMgr(__RPC__deref_out_opt ITfDocumentMgr **ppDm);
HRESULT CreateRangeBackup(TfEditCookie ec, __RPC__in_opt ITfRange *pRange, __RPC__deref_out_opt ITfRangeBackup **ppBackup);
</code></pre>
<p>暂且不详细解释每一个函数的作用，先挑代码中用到的函数讲解。在 SampleIME 项目中，当用户按下了按键之后，程序开始处理按键类型，并确定按键处理模式，然后使用当前线程中的 ITfContext 对象调用 RequestEditSession 函数接口。</p>
<pre><code>CKeyHandlerEditSession* pEditSession = nullptr;
HRESULT hr = E_FAIL;

// 这里 SampleIME 创建了一个自定义的 CKeyHandlerEditSession （继承于 ITfEditSession）
// we'll insert a char ourselves in place of this keystroke
pEditSession = new (std::nothrow) CKeyHandlerEditSession(this, pContext, code, wch, keyState);
if (pEditSession == nullptr) { return hr; }

// 调用 ITfContext::RequestEditSession 函数
// _tfClientId 是 ITfTextInputProcessor::Activate 调用时获取的
// Call CKeyHandlerEditSession::DoEditSession().
// Do not specify TF_ES_SYNC so edit session is not invoked on WinWord
hr = pContext-&gt;RequestEditSession(_tfClientId, pEditSession, TF_ES_ASYNCDONTCARE | TF_ES_READWRITE, &amp;hr);

pEditSession-&gt;Release();
</code></pre>
<p>ITfContext::RequestEditSession dwFlags 解释 <a target="_blank" rel="noopener" href="https://docs.microsoft.com/en-us/windows/win32/api/msctf/nf-msctf-itfcontext-requesteditsession">ITfContext::RequestEditSession method (msctf.h)</a></p>
<table>
<thead>
<tr>
<th>DWFlag</th>
<th>解释</th>
</tr>
</thead>
<tbody>
<tr>
<td>TF_ES_SYNC</td>
<td>编辑会话必须同步，否则请求将失败（返回错误代码：TF_E_synchronous）。此标志只应在有记录的情况下使用，在这种情况下，它“应该会”成功。否则，调用可能会失败。此值不能与TF_ES_ASYNCDONTCARE或TF_ES_ASYNC值组合。</td>
</tr>
<tr>
<td>TF_ES_ASYNC</td>
<td>编辑会话必须是异步的，否则请求失败。此值不能与TF_ES_ASYNCDONTCARE或TF_ES_SYNC值组合。</td>
</tr>
<tr>
<td>TF_ES_ASYNCDONTCARE</td>
<td>编辑会话可以同步或异步进行，由TSF管理器决定。管理器将尝试安排同步编辑会话以提高性能。此值不能与TF_ES_ASYNC或TF_ES_SYNC值组合。</td>
</tr>
<tr>
<td>TF_ES_READ</td>
<td>ITfContext 的内容对于 ITfEditSession 是只读性质的</td>
</tr>
<tr>
<td>TF_ES_READWRITE</td>
<td>ITfContext 的内容对于 ITfEditSession 是可读写的</td>
</tr>
</tbody>
</table>
<h3 id="4-3-2-ITfEditSession">4.3.2 ITfEditSession</h3>
<p>ITfEditSession 只提供了一个函数接口，当调用 ITfContext::RequestEditSession 函数时，由系统调用 ITfEditSession::DoEditSession，并创建一个 TfEditCookie，在 ITfEditSession 执行过程中（应该称为生命周期感觉更好点）可以通过 TfEditCookie 访问 ITfContext 里的内容。</p>
<pre><code>// ITfEditSession 函数接口
HRESULT STDMETHODCALLTYPE DoEditSession(TfEditCookie ec);

// ITfContext 中必须使用 TfEditCookie 的函数
ITfContext::GetSelection
ITfContext::SetSelection
ITfContext::GetStart
ITfContext::GetEnd
ITfContext::CreateRangeBackup

// ITfComposition 中必须使用 TfEditCookie 的函数
ITfComposition::ShiftStart
ITfComposition::ShiftEnd
ITfComposition::EndComposition

// ITfContextView 中必须使用 TfEditCookie 的函数
ITfContextView::GetRangeFromPoint
ITfContextView::GetTextExt

// ITfRange 中所有函数
// ITfProperty 中所有函数
// ITfContextComposition 中所有函数
// ITfInsertAtSelection 中所有函数

// 更新于 20220120 目前仅整理出 SampleIME 项目中所用到的函数（可能有遗漏...）
</code></pre>
<hr>
<p>更新于 2022年1月22号 未完待续…</p>
</article><section class="jump-container is-flex is-justify-content-space-between my-6"><!-- em is empty placeholder--><em></em></section></div></div></main></main><footer class="is-flex is-flex-direction-column is-align-items-center is-flex-shrink-0 is-family-serif"><section class="sns-container"><!-- RSS--><!-- Github--><!-- 知乎--><!-- gitee--><!-- bilibili--><!-- steam--></section><p><span>Copyright ©</span><span> 录石匠 2022</span></p><div class="is-flex is-justify-content-center is-flex-wrap-wrap"><p>Powered by Hexo &verbar;&nbsp;</p><p class="is-flex is-justify-content-center"><a title="Hexo theme author" target="_blank" rel="noopener" href="//github.com/haojen">Theme by Haojen&nbsp;</a></p><div style="margin-top: 2px"><a class="github-button" title="github-button" target="_blank" rel="noopener" href="https://github.com/haojen/hexo-theme-Claudia" data-color-scheme="no-preference: light; light: light; dark: dark;" data-show-count="true"></a></div></div><div><span></span></div></footer><script async defer src="https://buttons.github.io/buttons.js"></script><script src="/js/post.js"></script></body></html>